﻿using System;
using System.Collections.Generic;

namespace Notoric.Model.Packaging
{
    /// <summary>
    /// Implements a central authority responsible for assigning unique IDs to objects.
    /// </summary>
    public class IDAuthority
    {
        #region Constructors

        /// <summary>
        /// Creates a new <see cref="IDAuthority"/> instance.
        /// </summary>
        public IDAuthority()
        {
            ids = new Dictionary<Guid, object>();
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Finds the object that corresponds to a given id.
        /// </summary>
        /// <param name="objID">
        /// <para>
        /// Type: <see cref="Guid"/>
        /// </para>
        /// <para>
        /// The identifier of the object being searched.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="object"/>
        /// </para>
        /// <para>
        /// The object with the given <paramref name="objID"/> or <see langword="null"/> if none is found.
        /// </para>
        /// </returns>
        public object FindObject(Guid objID)
        {
            object obj = null;
            ids.TryGetValue(objID, out obj);

            return obj;
        }

        /// <summary>
        /// Finds the object that corresponds to a given id and casts it to <typeparamref name="T"/>.
        /// </summary>
        /// <param name="objID">
        /// <para>
        /// Type: <see cref="Guid"/>
        /// </para>
        /// <para>
        /// The identifier of the object being searched.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <typeparamref name="T"/>
        /// </para>
        /// <para>
        /// The object with the given <paramref name="objID"/> or <see langword="null"/> if none is found.
        /// </para>
        /// </returns>
        /// <typeparam name="T">The type of the object being searched</typeparam>
        /// <exception cref="InvalidCastException">
        /// The object with the given <paramref name="objID"/> was found but could not be cast to <typeparamref name="T"/>.
        /// </exception>
        public T FindObject<T>(Guid objID)
        {
            return (T)FindObject(objID);
        }

        /// <summary>
        /// Generates an ID for a given object and inserts it in the map.
        /// </summary>
        /// <param name="obj">
        /// <para>
        /// Type: <see cref="Object"/>
        /// </para>
        /// The inserted object.
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="Guid"/>
        /// </para>
        /// <para>
        /// The ID under which the object was inserted in the map.
        /// </para>
        /// </returns>
        /// <exception cref="ArgumentNullException">
        /// The <paramref name="obj"/> is <see langworg="null"/>.
        /// </exception>
        /// <exception cref="OutOfMemoryException">
        /// The map is full.
        /// </exception>
        public Guid GenerateID(object obj)
        {
            if (obj == null)
                throw new ArgumentNullException("obj");
            if (ids.Count == int.MaxValue)
                throw new OutOfMemoryException("The map is full.");

            var id = Guid.NewGuid();

            while (ids.ContainsKey(id))
                id = Guid.NewGuid();

            ids[id] = obj;

            return id;
        }

        /// <summary>
        /// Inserts an object to the ID map under a given ID.
        /// </summary>
        /// <remarks>
        /// This method is intended for use during the deserialization process.
        /// The deserialized object calls this method once it is recreated
        /// and inserts itself inside under the deserialized id.
        /// </remarks>
        /// <param name="obj">
        /// <para>
        /// Type: <see cref="Object"/>
        /// </para>
        /// <para>
        /// The inserted object.
        /// </para>
        /// </param>
        /// <param name="objID">
        /// <para>
        /// Type: <see cref="Guid"/>
        /// </para>
        /// <para>
        /// The identifier under which the object is being inserted.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// The <paramref name="obj"/> is <see langword="null"/>.
        /// </exception>
        /// <exception cref="ArgumentException">
        /// The map already contains another object under the same <paramref name="objID"/>.
        /// </exception>
        public void InsertObject(object obj, Guid objID)
        {
            if (obj == null)
                throw new ArgumentNullException("obj");
            if (ids.ContainsKey(objID) && ids[objID] != obj)
                throw new ArgumentException("The ID map already contains another object under the same ID!");

            ids[objID] = obj;
        }

        /// <summary>
        /// Removes the object with a given id from the map.
        /// </summary>
        /// <param name="id">
        /// <para>
        /// Type: <see cref="Guid"/>
        /// </para>
        /// <para>
        /// The ID of the removed object.
        /// </para>
        /// </param>
        public void RemoveObject(Guid id)
        {
            ids.Remove(id);
        }

        #endregion

        #region Private Fields

        /// <summary>
        /// Holds a map of assigned IDs.
        /// </summary>
        private Dictionary<Guid, object> ids;

        #endregion
    }
}
